package codeblocks;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import renderable.RenderableBlock;
import workspace.Workspace;
import workspace.WorkspaceListener;

/**
 * <code>BlockLinkChecker</code> determines if two <code>Block</code> objects
 * can connect. In particular, <code>BlockLinkChecker</code> will report which
 * sockets of the two <code>Block</code> objects can connect. Interested
 * <code>Block</code> objects may make a static call to canLink() to determine
 * if it can link to another <code>Block</code> object.
 * 
 * <code>BlockLinkChecker</code> uses a list of <code>LinkRule</code>s to check
 * the <code>Connector</code>s of each <code>Block</code>. Rules may be added,
 * inserted, and removed from the checker.
 * 
 * There is only one instance of the <code>BlockLinkChecker</code>.
 */
public class BlockLinkChecker {

	private static ArrayList<LinkRule> rules = new ArrayList<LinkRule>();

	// TODO get a better value
	private static double MAX_LINK_DISTANCE = 20.0;

	/**
	 * Clears all the rules within this.
	 */
	public static void reset() {
		rules.clear();
	}

	/**
	 * Adds a rule to the end of this checker's list of rules. If the rule
	 * already exists in the rule list, the rule is removed in the original
	 * location and added to the end of the list.
	 * 
	 * @param rule
	 *            the desired LinkRule to be added
	 */
	public static void addRule(LinkRule rule) {
		rules.add(rule);
		if (rule instanceof WorkspaceListener)
			Workspace.getInstance().addWorkspaceListener(
					(WorkspaceListener) rule);
	}

	/**
	 * Insert rule at the specified index in this checker's list of rules. The
	 * original rule at the specified index and rules after it are shifted down
	 * the list. If the index is greater or equal to the length of the rule
	 * list, then the rule is added to the end of the list. If the rule already
	 * exists in the rule list, the rule is moved to the specified index.
	 * 
	 * @param rule
	 *            the desired rule to insert
	 * @param index
	 *            the index to insert the rule in
	 */
	public static void insertRule(LinkRule rule, int index) {
		rules.remove(rule);
		rules.add(index, rule);
	}

	/**
	 * Removes the specified rule from the rule list
	 * 
	 * @param rule
	 *            the desired LinkRule to remove
	 */
	public static void removeRule(LinkRule rule) {
		rules.remove(rule);
	}

	/**
	 * Returns a BlockLink instance if the two specified blocks can connect at
	 * the specified block connectors at each block; null if no link is
	 * possible.
	 * 
	 * @param block1
	 *            Block instance to compare
	 * @param block2
	 *            Block instance to compare
	 * @param con1
	 *            the BlockConnector at block1 to compare against con2
	 * @param con2
	 *            the BlockConnector at block2 to compare against con1
	 */
	public static BlockLink canLink(Block block1, Block block2,
			BlockConnector con1, BlockConnector con2) {
		if (checkRules(block1, block2, con1, con2))
			return BlockLink.getBlockLink(block1, block2, con1, con2);

		return null;
	}

	/**
	 * Checks to see if a <code>RenderableBlock</code>s can connect to other
	 * <code>RenderableBlock</code>s. This would mean that they have
	 * <code>BlockConnector</code>s that satisfy at least one of the
	 * <code>LinkRule</code>s, and that these sockets are in close proximity.
	 * 
	 * @param rblock1
	 *            one of the blocks to check
	 * @param otherBlocks
	 *            the other blocks to check against
	 * @return a <code>BlockLink</code> object that gives the two closest
	 *         matching <code>BlockConnector</code>s in these blocks, or null if
	 *         no such matching exists.
	 */
	public static BlockLink getLink(RenderableBlock rblock1,
			Iterable<RenderableBlock> otherBlocks) {
		Block block1 = Block.getBlock(rblock1.getBlockID());
		BlockConnector closestSocket1 = null;
		BlockConnector closestSocket2 = null;
		Block closestBlock2 = null;
		double closestDistance = MAX_LINK_DISTANCE;
		double currentDistance;

		for (RenderableBlock rblock2 : otherBlocks) {
			BlockConnector currentPlug = getPlugEquivalent(block1);
			Block block2 = Block.getBlock(rblock2.getBlockID());
			if (block1.equals(block2) || !rblock1.isVisible()
					|| !rblock2.isVisible() || rblock1.isCollapsed()
					|| rblock2.isCollapsed())
				continue;

			Point2D currentPlugPoint = null;
			Point2D currentSocketPoint = null;
			if (currentPlug != null) {
				currentPlugPoint = getAbsoluteSocketPoint(rblock1, currentPlug);
				for (BlockConnector currentSocket : getSocketEquivalents(block2)) {
					currentSocketPoint = getAbsoluteSocketPoint(rblock2,
							currentSocket);
					currentDistance = currentPlugPoint
							.distance(currentSocketPoint);
					// if the block2 is a CustomMethod, the currentSocket will
					// no te the same inside checkRules!..it will be changed!!
					if ((currentDistance < closestDistance)
							&& checkRules(block1, block2, currentPlug,
									currentSocket)) {
						closestBlock2 = block2;
						closestSocket1 = currentPlug;
						closestSocket2 = currentSocket;
						closestDistance = currentDistance;
					}
				}
			}

			currentPlug = getPlugEquivalent(block2);
			if (currentPlug != null) {
				currentPlugPoint = getAbsoluteSocketPoint(rblock2, currentPlug);
				for (BlockConnector currentSocket : getSocketEquivalents(block1)) {
					currentSocketPoint = getAbsoluteSocketPoint(rblock1,
							currentSocket);
					currentDistance = currentPlugPoint
							.distance(currentSocketPoint);
					if ((currentDistance < closestDistance)
							&& checkRules(block1, block2, currentSocket,
									currentPlug)) {
						closestBlock2 = block2;
						closestSocket1 = currentSocket;
						closestSocket2 = currentPlug;
						closestDistance = currentDistance;
					}
				}
			}
		}

		if (closestSocket1 == null) {
			return null;
		}

		return BlockLink.getBlockLink(block1, closestBlock2, closestSocket1,
				closestSocket2);
	}

	/**
	 * NOTE: ALWAYS prefer BlockLinkChecker.getLink over this method.
	 * 
	 * Checks to see if a <code>RenderableBlock</code>s can connect to other
	 * <code>RenderableBlock</code>s, implying that rblock1 has at least one
	 * <code>BlockConnector</code>s that satisfies at least one of the
	 * <code>LinkRule</code>s.
	 * 
	 * Does not require close proximity.
	 * 
	 * @param rblock1
	 *            one of the blocks to check
	 * @param otherBlocks
	 *            the other blocks to check against
	 * @return a <code>BlockLink</code> object that gives the two closest
	 *         matching <code>BlockConnector</code>s in these blocks, or null if
	 *         no such matching exists.
	 */
	public static BlockLink getWeakLink(RenderableBlock rblock1,
			Iterable<RenderableBlock> otherBlocks) {
		Block block1 = Block.getBlock(rblock1.getBlockID());
		BlockConnector closestSocket1 = null;
		BlockConnector closestSocket2 = null;
		Block closestBlock2 = null;
		double closestDistance = Double.POSITIVE_INFINITY;
		double currentDistance;

		for (RenderableBlock rblock2 : otherBlocks) {
			BlockConnector currentPlug = getPlugEquivalent(block1);
			Block block2 = Block.getBlock(rblock2.getBlockID());
			if (block1.equals(block2) || !rblock1.isVisible()
					|| !rblock2.isVisible())
				continue;

			Point2D currentPlugPoint = null;
			Point2D currentSocketPoint = null;
			if (currentPlug != null) {
				currentPlugPoint = getAbsoluteSocketPoint(rblock1, currentPlug);
				for (BlockConnector currentSocket : getSocketEquivalents(block2)) {
					currentSocketPoint = getAbsoluteSocketPoint(rblock2,
							currentSocket);
					currentDistance = currentPlugPoint
							.distance(currentSocketPoint);
					if ((currentDistance < closestDistance)
							&& checkRules(block1, block2, currentPlug,
									currentSocket)) {
						closestBlock2 = block2;
						closestSocket1 = currentPlug;
						closestSocket2 = currentSocket;
						closestDistance = currentDistance;
					}
				}
			}

			currentPlug = getPlugEquivalent(block2);
			if (currentPlug != null) {
				currentPlugPoint = getAbsoluteSocketPoint(rblock2, currentPlug);
				for (BlockConnector currentSocket : getSocketEquivalents(block1)) {
					currentSocketPoint = getAbsoluteSocketPoint(rblock1,
							currentSocket);
					currentDistance = currentPlugPoint
							.distance(currentSocketPoint);
					if ((currentDistance < closestDistance)
							&& checkRules(block1, block2, currentSocket,
									currentPlug)) {
						closestBlock2 = block2;
						closestSocket1 = currentSocket;
						closestSocket2 = currentPlug;
						closestDistance = currentDistance;
					}
				}
			}
		}

		if (closestSocket1 == null) {
			return null;
		}

		return BlockLink.getBlockLink(block1, closestBlock2, closestSocket1,
				closestSocket2);
	}

	/**
	 * Checks if a potential link satisfies ANY of the rules loaded into the
	 * link checker
	 * 
	 * @param block1
	 *            one Block in the potential link
	 * @param block2
	 *            the other Block
	 * @param socket1
	 *            the BlockConnector from block1 in the potential link
	 * @param socket2
	 *            the BlockConnector from block2
	 * @return true if the pairing of block1 and block2 at socket1 and socket2
	 *         passes any rules, false otherwise
	 */
	private static boolean checkRules(Block block1, Block block2,
			BlockConnector socket1, BlockConnector socket2) {
		Iterator<LinkRule> rulesList = Collections.unmodifiableList(rules)
				.iterator();
		LinkRule currentRule = null;
		boolean foundRule = false;
		while (rulesList.hasNext()) {
			currentRule = rulesList.next();
			// in case of Method parameter:
			// block1 -> the number parameter
			// block2 -> the "procedure"
			// socket1 -> kind: number
			// socket2 -> kind: poly
			// TODO llamarlo is MethodDecl!
			// if (block2.isProcedureDeclBlock() ||
			// block2.isVariableDeclBlock())
			// if (block2.isDeclaration()|| block2.isReturnBlock()){
			// socket2.setKind(socket1.getKind());
			// //
			// RenderableBlock.getRenderableBlock(block1.getBlockID()).getBlockShape().makeTopSide();
			// //
			// RenderableBlock.getRenderableBlock(block1.getBlockID()).getBlockShape().makeBottomSide();
			// //block2.setReturnType(TypeConverter.get(socket1.getKind()));
			// }

			// the return block change the shape of the custom method
			// if (block2.isReturnBlock()){
			// //search the Custom Method
			// // Block
			// start=Block.getBlock(Block.getBlock(block2.getBlockID()).getBeforeBlockID());
			// // Block previous=null;
			// // while(start!=null){
			// // previous=start;
			// // start=Block.getBlock(start.getBeforeBlockID());
			// // }
			// Block previous=Block.getStarterMethod(block2);
			// //previus is now the Custom Method
			// if (previous!=null){
			// previous.changeShapePlug(socket1.getKind());
			// }
			// }

			boolean canLink = currentRule.canLink(block1, block2, socket1,
					socket2);
			if (!currentRule.isMandatory())
				foundRule |= canLink;
			else if (!canLink)
				return false;
		}
		return foundRule;
	}


	/**
	 * Gets the screen coordinate of the center of a socket.
	 * 
	 * @param block
	 *            the RenderableBlock containting the socket
	 * @param socket
	 *            the desired socket
	 * @return a Point2D that represents the center of the socket on the screen.
	 */
	private static Point2D getAbsoluteSocketPoint(RenderableBlock block,
			BlockConnector socket) {
		Point2D relativePoint = block.getSocketPixelPoint(socket);
		Point2D blockPosition = block.getLocationOnScreen();
		return new Point2D.Double(relativePoint.getX() + blockPosition.getX(),
				relativePoint.getY() + blockPosition.getY());
	}

	public static boolean hasPlugEquivalent(Block b) {
		if (b == null)
			return false;
		boolean hasPlug = b.hasPlug();
		boolean hasBefore = b.hasBeforeConnector();
		// Should have at most one plug-type connector
		assert (!(hasPlug & hasBefore));
		return hasPlug | hasBefore;
	}

	public static BlockConnector getPlugEquivalent(Block b) {
		if (!hasPlugEquivalent(b))
			return null;
		if (b.hasPlug())
			return b.getPlug();
		return b.getBeforeConnector();
	}

	public static Iterable<BlockConnector> getSocketEquivalents(Block b) {
		if (b == null)
			return new ArrayList<BlockConnector>();
		if (!b.hasAfterConnector())
			return b.getSockets();
		ArrayList<BlockConnector> socketEquivalents = new ArrayList<BlockConnector>();
		for (BlockConnector socket : b.getSockets()) {
			socketEquivalents.add(socket);
		}
		socketEquivalents.add(b.getAfterConnector());
		return Collections.unmodifiableList(socketEquivalents);
	}

	/**
	 * Prints to the console all the rules this LinkChecker currently supports.
	 */
	public static void printRules() {

	}

}
